-
Notifications
You must be signed in to change notification settings - Fork 212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: fusdc status manager states #10406
Conversation
Deploying agoric-sdk with Cloudflare Pages
|
af07f34
to
a4ae691
Compare
// TODO FAILURE PATH -> put money in recovery account or .transfer to receiver | ||
// TODO should we have a TxStatus for this? | ||
throw makeError( | ||
`🚨 No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this recoverable? It looks like it.
`🚨 No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`, | |
`⚠️ No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`, |
per Logging docs:
🚨 indicates something that should never happen.
⚠️ indicates something that is not expected but was recovered from.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚨 was intentional - I think we should leave it until we recover from here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might want to capture this in the StatusManager with an ORPHANED
state, and add some logic in the observe()
method to initiate a SETTLE
/ settlement
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's come back to this when we're going all the error cases
...mutableEntries[unsettledIdx], | ||
status: TxStatus.SETTLED, | ||
}; | ||
txs.set(key, harden(mutableEntries)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we ever delete from txs
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the approach to remove entries once they get to Settled
* @param {bigint} amount | ||
*/ | ||
settle(address, amount) { | ||
const key = `${address}-${amount}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The exo guard doesn't guarantee that address
includes no -
characters. This sets off my confusion-attack spidey-sense.
Consider...
const key = `${address}-${amount}`; | |
const key = JSON.stringify([address, `${amount}`]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please see this fixup: 5226323
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another alternative: put the number first
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or split from the right? Hm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no reqs currently to decompose the key, so I think the current approach is sound. If anything, JSON.stringify
might be overkill vs simple concatenation.
This is now factored to its own function and easy to change in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no reqs currently to decompose the key
Good point. It could even be a hash function. Let's leave it as is for debug-ability and we can button up before release
4cb1978
to
5226323
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good stuff! Most of my feedback I'd be content to see a TODO so that we can land this and keep iterating.
*/ | ||
handleEvent(event) { | ||
// observe regardless of validation checks | ||
// TODO - should we only observe after validation checks? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah it's not a transaction unless it's valid.
but validation checks should be performed by the Feed exo so it only forwards what is legitimate. The advancer needn't be concerned with query params.
There's no Feed yet so this could have a TODO but please make the plan clear. Perhaps add it to the issue about making the feed
return 0; | ||
} | ||
const current = txs.get(key); | ||
// XXX do we need defensive logic to ensure multiple entries don't have |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a fine invariant to assert. it's recoverable by ignoring duplicates. we don't put that burden on the feed because it would have to keep a record of all transactions. this storage can do the job
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I'll save that for a later PR, but we can use txHash
+ chainId
to assert uniqueness.
Thinking we'd want a new SetStore to keep track of these, since the txs MapStore
ejects entries once they're Settled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented in 6883b74
txs.set(key, harden(mutableEntries)); | ||
}, | ||
/** | ||
* @overload |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL! you can also do this in a TS expression inside a @type
comment. give that a try because it would be harder to miss. (My eyes only looked up to the lowest /**
and didn't think to keep searching)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok iIll try that. This was a pain to wrangle and I wasn't happy with the current outcome
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No luck with this:
/**
* @type {((address: NobleAddress, amount: bigint) => StatusManagerValue[]) | ((address: NobleAddress, amount: bigint, index: number) => StatusManagerValue | undefined)}
*/
Was also playing around with these:
/**
* @type {<T extends number | undefined = undefined>(
* address: NobleAddress,
* amount: bigint,
* index?: T
* ) => T extends number ? StatusManagerValue | undefined : StatusManagerValue[]}
*/
/**
* @template {number | undefined} [T=undefined]
* @param {NobleAddress} address
* @param {bigint} amount
* @param {T} [index]
* @returns {T extends number ? StatusManagerValue | undefined : StatusManagerValue[]}
*/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also don't know if the index
logic in StatusManager
is here to say. Some of the convos in other threads lead me to believe we won't need it. So not going to spin wheels too hard on this rn.
Edit: the index logic did not stay around, so this is irrelevant
import { makeError, q } from '@endo/errors'; | ||
|
||
/** | ||
* Very minimal 'URL query string'-like parser that handles: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we not have access to URL
in XS?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure - but agoric123?foo=bar
is not accepted in a URL constructor in a web console.
qs
, query-string
, and querystringify
are well-known libraries we might consider using if this were to be provided by @agoric/orchestration
. This particular contract seems to have much simpler reqs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we don't have URL in XS, I'm pretty sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Glad to see the tests.
qs-lite.min.js in https://github.com/kawanet/qs-lite is 530 bytes. Maybe easier to source that to get a more conventional qs ability
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, xs seems to have decodeURIComponent
too: https://github.com/Moddable-OpenSource/moddable/blob/978945595c6aaa460c6068b34d3ebc415db248a7/xs/sources/xsCommon.c#L1375
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose we can get by without decodeURIComponent
for now but consider mentioning in the code that it's available in XS
444aedd
to
88f51f3
Compare
packages/fast-usdc/src/constants.js
Outdated
* | ||
* @enum {(typeof WriterTxStatus)[keyof typeof WriterTxStatus]} | ||
*/ | ||
export const WriterTxStatus = /** @type {const} */ ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
helpful distinction. please make the unqualified name be this all-encompassing enum. Observed
and Advanced
are the subset of a transaction status that are recorded in local state.
Settled --> [*] | ||
``` | ||
|
||
### Complete state diagram (starting from OCW) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WFM
/** @type {ChainAddress} */ | ||
const destination = harden({ | ||
chainId, | ||
value: EUD, | ||
encoding: /** @type {const} */ ('bech32'), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably worth a helper fn
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, what would we call it? toChainAddress might not capture the bech32
we're adding. toBech32ChainAddress
seemed too verbose where it wasn't worth factoring out.
Maybe ChainHub's getChainInfoByAddress
should do this and instead be getChainAddress(addr)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to putting it on ChainHub
since chainId
is used here only for this construction.
We shouldn't reuse getChainInfoByAddress
because this is making a value, not getting one.
consider,
/** @type {ChainAddress} */ | |
const destination = harden({ | |
chainId, | |
value: EUD, | |
encoding: /** @type {const} */ ('bech32'), | |
}); | |
const destination = chainHub.makeChainAddress(EUD); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, please see 990a6df. I decided not to label this a breaking change since getChainInfoByAddress
has only been around for ~a week, but let me know if I should amend the commit message.
observe: M.call(CctpTxEvidenceShape).returns(M.undefined()), | ||
hasPendingSettlement: M.call(M.string(), M.bigint()).returns(M.boolean()), | ||
settle: M.call(M.string(), M.bigint()).returns(M.undefined()), | ||
view: M.call(M.string(), M.bigint()).returns(M.arrayOf(PendingTxShape)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a new name for what is usually called lookup
. It also reads as if it could be a state transition. Please change.
assertNotSeen(evidence); | ||
const key = getPendingTxKey(evidence); | ||
const entry = { ...evidence, status: TxStatus.Observed }; | ||
|
||
appendToStoredArray( | ||
// @ts-expect-error index signature for type 'string' is missing in type 'PendingTx'. | ||
pendingTxs, | ||
key, | ||
entry, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please DRY the duplication with advance
using a helper in the prepare scope. e.g.
assertNotSeen(evidence); | |
const key = getPendingTxKey(evidence); | |
const entry = { ...evidence, status: TxStatus.Observed }; | |
appendToStoredArray( | |
// @ts-expect-error index signature for type 'string' is missing in type 'PendingTx'. | |
pendingTxs, | |
key, | |
entry, | |
); | |
addTransaction(evidence, TxStatus.Observed); |
import { makeError, q } from '@endo/errors'; | ||
|
||
/** | ||
* Very minimal 'URL query string'-like parser that handles: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Glad to see the tests.
qs-lite.min.js in https://github.com/kawanet/qs-lite is 530 bytes. Maybe easier to source that to get a more conventional qs ability
aa7d0dd
to
6883b74
Compare
* @param {CctpTxEvidence} evidence | ||
*/ | ||
handleTransactionEvent(evidence) { | ||
// XXX assume EventFeed performs validation checks. Advancer will assert uniqueness. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not tech debt, this is part of the design
config => harden(config), | ||
{ | ||
/** | ||
* XXX For now, assume this is invoked when a new entry is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why make this assumption? The advancer should not be concerned
requestedAmount, | ||
); | ||
|
||
// mark as Advanced since we're initiating the advance |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
already started
// mark as Advanced since we're initiating the advance | |
// mark as Advanced since `.transfer` initiates the advance |
packages/fast-usdc/src/types.ts
Outdated
/** composite of NobleAddress and transaction amount value */ | ||
export type PendingTxKey = `"["${NobleAddress}",${bigint}]"`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should not be parsable.
/** composite of NobleAddress and transaction amount value */ | |
export type PendingTxKey = `"["${NobleAddress}",${bigint}]"`; | |
/** composite of NobleAddress and transaction amount value */ | |
export type PendingTxKey = `"pendingTx:${string}`; |
* @param {bigint} amount | ||
*/ | ||
const toPendingTxKey = (addr, amount) => | ||
/** @type {PendingTxKey} */ (JSON.stringify([addr, String(amount)])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please make clear in the docs that this isn't meant to be parseable. see also PendingTxKey
type comment
import { makeError, q } from '@endo/errors'; | ||
|
||
/** | ||
* Very minimal 'URL query string'-like parser that handles: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose we can get by without decodeURIComponent
for now but consider mentioning in the code that it's available in XS
* @returns {boolean} | ||
*/ | ||
hasQueryParams: address => { | ||
return address.includes('?'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agoric1foo?bar?baz
would return true but is invalid. for this to be true it getQueryParams
should not error.
return address.includes('?'); | |
try { | |
addressTools.getQueryParams(address); | |
return result.params.length > 0; | |
} catch { | |
return false; | |
} |
const txStatuses = values(TxStatus); | ||
const invalidStatuses = values(PendingTxStatus).find( | ||
status => !txStatuses.includes(status), | ||
); | ||
t.is(invalidStatuses, undefined, `${invalidStatuses} not in TxStatus`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thrown off by the comparison with undefined
.
const txStatuses = values(TxStatus); | |
const invalidStatuses = values(PendingTxStatus).find( | |
status => !txStatuses.includes(status), | |
); | |
t.is(invalidStatuses, undefined, `${invalidStatuses} not in TxStatus`); | |
const allStatusus = new Set(values(TxStatus)); | |
const pendingStatuses = new Set(values(PendingTxStatus)); | |
const pendingMinusAll = pendingStatuses.difference(allStatuses); | |
t.is(pendingMinusAll.size, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main intention here was to ensure the extraneous value name(s) are in the error message. TIL about Set.difference
, pretty cool. Unfortunately in our testing env, I'm seeing pendingStatuses.difference is not a function. I went with a slightly revised approach that's hopefully more readable: 9c14032
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, it's not available until Node 22. When we can use it, isSubsetOf would be even better. But the filter is fine.
8acd20c
to
a92b0ec
Compare
}, | ||
|
||
/** | ||
* Lookup all entries for a given address and amount |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all the pending entries. maybe also lookupPending
in the name
|
||
export type LogFn = (...args: unknown[]) => void; | ||
|
||
export interface PendingTx extends CctpTxEvidence { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should do more of this, interfaces extending interfaces
- removes `getChainInfoByAddress` and provides `makeChainAddress`, better aligning the helper with intended usage - arguably a breaking change, but we've yet to release `getChainInfoByAddress`
a92b0ec
to
9eac623
Compare
- shows the various `StatusManager` MapStore states updates (`OBSERVED`, `ADVANCED`) via `Advancer` and `Settler` stubs
- use a composite key of `txHash+chainId` to track unique `EventFeed` submissions
9eac623
to
f3d1e36
Compare
closes: #10389
Description
StatusManager
withOBSERVED
,ADVANCED
states tracked in localMapStore
StatusManager
state updates viaSettler
andAdvancer
CctpTxEvidence
type, typeGuard, and fixturesSecurity Considerations
See FastUSDC threat model
Scaling Considerations
seenTxs
SetStore that will keep track of every tx observed by fusdc contract to assert uniquenessDocumentation Considerations
Includes state diagram in exos/README.md and the usual jsdoc
Testing Considerations
Includes unit tests of exos with a mocked LCA.
Upgrade Considerations
None, unreleased